Skip to content

Add shopify store info --store <domain> command#7660

Draft
amcaplan wants to merge 24 commits into
ariel/cli-kit-url-helpersfrom
ariel/store-info-command
Draft

Add shopify store info --store <domain> command#7660
amcaplan wants to merge 24 commits into
ariel/cli-kit-url-helpersfrom
ariel/store-info-command

Conversation

@amcaplan
Copy link
Copy Markdown
Contributor

@amcaplan amcaplan commented May 28, 2026

📚 Part of a stack — review/merge bottom-up:

  1. Add a shared GID codec to cli-kit and adopt it in app and organizations #7753 — shared GID codec in cli-kit (adopted by app + organizations)
  2. Add extractHost and extractMyshopifyHandle to cli-kit/common/url #7754extractHost / extractMyshopifyHandle in cli-kit/common/url
  3. Add shopify store info --store <domain> command #7660 (this PR)shopify store info, which consumes both

The GID-decoding and URL-host helpers this command relies on were extracted into the two base PRs above so they aren't buried in a command-specific module. This PR's diff is the store layer only.

Summary

Adds shopify store info --store <domain>, which surfaces metadata about a store you have access to. Output is aligned to the store-management project brief's dev-store contract and every field is sourced from the Business Platform (BP Destinations + BP Organizations), so the command works without store auth — no Admin API access required.

Fields (camelCase in --json, labelled in text):

Field Source
id (gid://shopify/Shop/<id>) BP Organizations shopifyShopId (GID built locally)
displayName BP Organizations
subdomain --store
organizationId, organizationName BP Destinations → organizationForDestination
storeOwner { name, email } BP Organizations ownerDetails
type (dev / production / …) BP Store enum
plan (basic / grow / advanced / plus) BP Shop.planName, mapped to a public handle
featurePreview BP Organizations developerPreviewHandle
adminUrl derived from the --store domain
  • Supports --json for machine-readable output; text output renders a single Store details section.
  • Fields that can't be resolved are simply omitted — when the BP Organizations lookup fails, the destination-resolved fields (organizationId / organizationName) plus subdomain and adminUrl are still returned.
  • plan maps BP's raw internal plan names (which differ from marketing names, e.g. professional → Grow, unlimited → Advanced) to a public handle via a hardcoded 1:1 table; unrecognized plans are omitted.

Implementation notes

  • Two-query BP path: destinations(search:)currentUserAccount.organizationForDestination(destinationPublicId:), then BP Organizations accessibleShops(search:) matched on the canonical myshopify domain.
  • The destinations query fetches only publicId (to resolve the owning org) and the primaryDomain / webUrl used to match the store — every surfaced shop field comes from BP Organizations, and the admin GID / admin URL are derived locally rather than fetched.
  • Internal identifiers beyond the public id / organizationId GIDs are intentionally not surfaced — the CLI addresses stores by domain.

Commits

  1. Rework store info output to the Business Platform contract — camelCase doc contract, BP sourcing, and removal of the store auth/Admin data tier (service modules, GraphQL query + codegen project, and per-field error plumbing) along with the timezone / setup-status / Plus fields it backed.
  2. Add plan field to store info — the plan public-handle mapping.
  3. Fetch only the store-info fields we surface — trim the destinations query to what's actually consumed (matching + owning-org lookup) and derive the GID / admin URL locally.

Quality gates

  • pnpm vitest run (packages/store) — 168 tests passing
  • pnpm type-check
  • pnpm lint
  • pnpm refresh-manifests — manifest + README regenerated and committed
  • pnpm build-dev-docs — store info description is not embedded in generated_docs_data_v2.json; no change

Test plan

  • shopify store info --store <store>.myshopify.com returns the contract fields without store auth
  • --json output matches the camelCase contract
  • Missing --store surfaces a clear AbortError
  • Non-existent / inaccessible shop domain surfaces a clear AbortError
  • When the BP Organizations lookup fails, the org / admin-URL / subdomain baseline still renders

Out of scope / follow-ups

  • Preview stores — only BP-fetched store types are in scope here.
  • --store accepting a GID or integer — should be a separate CLI-wide change to storeFlags.store / normalizeStoreFqdn rather than localized to info. Interactive org/store prompting (which would make --store optional) is likewise deferred.

Closes shop/issues-develop#22724

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor Author

amcaplan commented May 28, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.


static descriptionWithMarkdown = `Reads metadata for a store from the Business Platform Destinations and Organizations APIs.

Tier 1 and Tier 2 fields work without \`store auth\`. Tier 3 fields (shop owner, timezone, features, setup required) require \`store auth\` and are only included when \`--verbose\` is set.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This reads like harness session language and could be clarified.

Comment thread packages/cli/README.md
-j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON. Automatically disables color output.
-s, --store=<value> (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to authenticate
against.
-s, --store=<value> (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are losing some info here + below. I am debating whether it's better to have a shared flag entirely, or there's value in overriding the description.

@amcaplan amcaplan changed the title Add shopify store info <store> command Add shopify store info --store <domain> command May 28, 2026
@amcaplan amcaplan force-pushed the ariel/extract-organizations-package branch from d67e8d3 to c9531e3 Compare May 28, 2026 22:02
@amcaplan amcaplan force-pushed the ariel/store-info-command branch from 38490df to 979bd9a Compare May 28, 2026 22:02
Base automatically changed from ariel/extract-organizations-package to main May 28, 2026 22:14
@amcaplan amcaplan force-pushed the ariel/store-info-command branch from 979bd9a to c1e7a69 Compare May 31, 2026 18:17
amcaplan and others added 17 commits June 8, 2026 17:29
Adds a new read-only command that surfaces shop metadata from BP Destinations
and BP Organizations APIs, with optional Admin API enrichment via `--verbose`.
Supports `--json` output and per-field graceful degradation via a
`_field_errors` envelope.

Tier 1 + Tier 2 fields work without `store auth` for the shop; Tier 3 fields
(shop_owner, timezone, features, setup_required) require `store auth` and are
opt-in via `--verbose`.

Fixes #22724

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces repeated `if (value) result.field = value` chains with a
single `compact()` pass from `@shopify/cli-kit/common/object`,
flattening buildResult, applyVerboseFields, buildPlan, and mapAdminShop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns with the rest of the store: namespace (e.g. store execute) which
takes the shop domain via `-s, --store` rather than a positional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoists the `-s, --store` flag definition into packages/store/src/cli/flags.ts
following the themeFlags/appFlags precedent. `store auth`, `store execute`,
and `store info` now share the same flag declaration.

Side effect: the per-command verbal description ("…to authenticate against",
"…to execute against", "…to inspect") collapses into "The myshopify.com
domain of the store." — visible only in --help. The verb is redundant with
the command name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BP's Destinations + Organizations APIs return primaryDomain/webUrl as
full URLs with scheme ("https://shop.myshopify.com"), not bare hosts.
Strict string equality against the requested store FQDN missed every
real shop. They also return `handle: null` and a non-subdomain
`shortName` (e.g. "ACT"), so admin_url construction had nothing to work
with.

- Extract host from URL fields before comparing (new info/host.ts).
- Derive the canonical myshopify subdomain from primaryDomain/webUrl
  and overwrite destination.handle with it so admin_url is built from
  the actual shop subdomain.
- Use primaryDomain (storefront URL) for primary_url instead of
  webUrl/url (both are admin URLs ending in /admin).
- Drop the misleading `url` field from OrganizationShopFields and the
  Org GraphQL selection — it's the admin URL, not the storefront.
- Correct features.branding type from boolean to string (it's an enum
  like "SHOPIFY", not a flag).

Verified live against ariel-caplan-test: Tier 1, 2, and 3 all populate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keys render as "Store Type" instead of "store_type", and GraphQL enum
strings like APP_DEVELOPMENT render as "App Development". Currency
codes like USD pass through unchanged.
CLI commands take --store as a domain, so BP/Core shop IDs and the
owning-org id have no follow-on use for users of this command.
is_main_shop is a billing-contract internal that almost no user cares
about. Removed from both text and JSON output; org id is kept as an
internal-only type to drive the BP Organizations request.
Local-time output without a timezone is ambiguous when sharing output
across regions; rendering in UTC makes timestamps unambiguous regardless
of where the user is.
Drop references to "Tier", "Identity", and "Business Platform" from the
command description, section titles, and error reasons — those are
internal taxonomy that users don't recognize.
--verbose is a global "show your work" flag (more diagnostic output)
across the CLI; using it to mean "include more result fields" is a
mismatched concept. --full reads naturally as "give me the full set
of fields".
Drop the features sub-object: storefront is uniformly true, branding is
cosmetic, harmonizedSystemCode is niche cross-border tooling. Lift
shopifyPlus to a top-level `plus` boolean — it overlaps with plan.name
but is relevant enough on its own to deserve a clean signal rather than
requiring callers to regex-match plan names.

--full now adds exactly: shop_owner, timezone, setup_required, plus.
…uthed

The flag was redundant: we already know whether the store is
authenticated by the time we compose the result, so gating the Admin
fetch behind an opt-in flag just added friction without unlocking
anything. Now `store info` quietly includes shop_owner/timezone/
setup_required/plus when `store auth` exists for the store, and omits
them otherwise. The `auth_status: not authenticated` field already
serves as the discoverability hint, so we don't pollute output with
field errors for fields the user never asked for.
Replace data-source-based section names (Overview / Plan & lifecycle /
Admin details) with categories users actually think in: Store, Access,
Plan, Activity. Timezone is a store property, so it sits in Store
rather than Activity.
The --verbose flag was removed when Admin-sourced fields became
automatic for authed shops; the test still asserted on it.
knip flagged a handful of types and option interfaces that were
exported but only referenced inside their own file. Also drop the
stale --full and Tier-1/2 comments from the result envelope.
Both @Shopify/organizations and the store info plumbing were decoding
base64-encoded organization GIDs. Pull the helper into the
organizations package's models module and export it, then have
store info import it.

Returns string | undefined so each caller can decide what to do on
failure: fetchOrganizations still aborts (the id is required to
display the org); store info silently omits the id (the BP
Organizations request would have failed with a garbage id anyway).
amcaplan and others added 7 commits June 8, 2026 17:29
The base64↔gid logic isn't organization-specific, so a helper named
decodeOrganizationGid in @Shopify/organizations was misleading.

Add three generic primitives in @shopify/cli-kit/common/gid:
- numericIdFromGid: extract /<digits> from a plain gid://...
- numericIdFromEncodedGid: same, but for base64-encoded gids
- encodeGid: base64-encode a plain gid

Thread these through every site that does the same work:
- @Shopify/organizations fetch.ts
- @shopify/store store info destinations
- app-management-client.ts (numberFromGid/idFromEncodedGid wrappers
  keep their domain semantics — auto-detect numeric strings,
  fail-fast on bad gids — but delegate the regex/base64 work)

Domain-validated extractors like extractBulkOperationId
(@shopify/app bulk operations) stay; they require a specific gid
prefix that the generic helper doesn't enforce.

Drops the store→organizations dependency that existed only for
this helper.
Neither helper was specific to store info — extractHost is a generic
URL→host parser, and extractMyshopifyHandle is Shopify-domain logic
that already has neighbors in cli-kit. Move both into
@shopify/cli-kit/common/url alongside isValidURL and safeParseURL,
add direct tests, and delete the store/info/host.ts module.

extractHost now uses safeParseURL to avoid a bare try/catch.
Follow the repo convention used by other packages: queries live in
.graphql files under src/cli/api/graphql/<api>/queries/, with typed
documents generated alongside. Replaces hand-written inline query
strings and hand-typed response interfaces with the businessPlatform-
RequestDoc / businessPlatformOrganizationsRequestDoc / adminRequestDoc
typed-document variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The schema files are sourced from external repos at codegen time (mirroring how packages/app and packages/organizations work) and shouldn't be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align `store info` to the store-management project brief's dev-store
contract, sourcing every field from the Business Platform: the result is
now camelCase (id, displayName, subdomain, organizationId,
organizationName, storeOwner{name,email}, type, featurePreview,
adminUrl) and `id` is a `gid://shopify/Shop/<id>` GID.

Owner and feature preview come from BP Organizations
(Shop.ownerDetails, Shop.developerPreviewHandle), so the command no
longer needs Admin access. This lets us drop the `store auth`/Admin data
tier entirely — its service modules, GraphQL query and codegen project,
and the per-field error plumbing — along with the timezone, setup
status, and Plus-subscription fields it backed.

`plan` is intentionally deferred (see the TODO notes) pending the
internal-name -> public-handle mapping decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the store's plan as a public handle (basic | grow | advanced |
plus) in both the text and `--json` output, sourced from BP
`Shop.planName`.

The raw plan names BP returns are Shopify-internal and differ from the
marketing names (e.g. `professional` is Grow, `unlimited` is Advanced),
so they're mapped through a hardcoded 1:1 table in `plan.ts`. Both the
internal name and the public handle are accepted as keys since the exact
form BP returns isn't pinned down by the schema. Plans not in the table
are treated as unrecognized and omitted from the output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BP destinations query pulled name/handle/shortName/isAppDevelopment to
back display-name/type/admin-url fallbacks, but the contract sources those
fields from the Organizations call. Trim the destinations query to publicId
(for the owning-org lookup) plus the primaryDomain/webUrl used to match the
store, and derive admin_url from the validated `--store` domain. The shop
GID is built locally from the numeric ShopifyShopID, so nothing is fetched
purely to format the `id`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@amcaplan amcaplan changed the base branch from main to graphite-base/7660 June 8, 2026 14:39
@amcaplan amcaplan force-pushed the ariel/store-info-command branch from 55cad39 to 77cbde9 Compare June 8, 2026 14:39
@amcaplan amcaplan changed the base branch from graphite-base/7660 to ariel/cli-kit-url-helpers June 8, 2026 14:39
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 8, 2026

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/public/common/gid.d.ts
/**
 * Extracts the trailing numeric id from a plain GraphQL global id like
 * `gid://shopify/Product/123`.
 *
 * @param gid - A plain GraphQL global id string.
 * @returns The trailing numeric id, or undefined when the string does not end with `/<digits>`.
 */
export declare function numericIdFromGid(gid: string): string | undefined;
/**
 * Decodes a base64-encoded GraphQL global id (for example, the form
 * Business Platform APIs return) and returns the trailing numeric id.
 *
 * @param gid - A base64-encoded GraphQL global id.
 * @returns The trailing numeric id, or undefined when the decoded string does not end with `/<digits>`.
 */
export declare function numericIdFromEncodedGid(gid: string): string | undefined;
/**
 * Encodes a plain GraphQL global id (`gid://...`) as base64, which is the
 * form some Business Platform endpoints require.
 *
 * @param gid - A plain GraphQL global id string to encode.
 * @returns The base64-encoded gid.
 */
export declare function encodeGid(gid: string): string;

Existing type declarations

packages/cli-kit/dist/public/common/url.d.ts
@@ -12,4 +12,20 @@ export declare function isValidURL(url: string): boolean;
  * @param url - The string to parse into a URL.
  * @returns A URL object if the parsing is successful, undefined otherwise.
  */
-export declare function safeParseURL(url: string): URL | undefined;
\ No newline at end of file
+export declare function safeParseURL(url: string): URL | undefined;
+/**
+ * Extracts the lowercased hostname from a URL-shaped string. Tolerates
+ * bare hosts (without a scheme) and inputs that come back from APIs as
+ * either  or .
+ *
+ * @param value - A URL or bare host string, possibly null/undefined.
+ * @returns The lowercased hostname, or undefined when the input is empty.
+ */
+export declare function extractHost(value: string | null | undefined): string | undefined;
+/**
+ * Extracts the subdomain handle from a  URL or host.
+ *
+ * @param value - A URL or host string, possibly null/undefined.
+ * @returns The myshopify subdomain handle, or undefined when the input isn't a  URL.
+ */
+export declare function extractMyshopifyHandle(value: string | null | undefined): string | undefined;
\ No newline at end of file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants